//~ Revision: 197, Copyright (C) 2014-2017: Willem Vree, contributions Stéphane David.
//~ This program is free software; you can redistribute it and/or modify it under the terms of the
//~ GNU General Public License as published by the Free Software Foundation; either version 2 of
//~ the License, or (at your option) any later version.
//~ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
//~ without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//~ See the GNU General Public License for more details. <http://www.gnu.org/licenses/gpl.html>.

var VERSION = 197;

var opt, onYouTubeIframeAPIReady, msc_credits, media_height, times_arr, offset_js, abc_arr,
    lpRec, media_file, abc_enc, play_list;
(function () {
    "use strict";
    var muziek, curmtr, curtmp, msc_svgs, msc_gs, msc_wz, offset, mediaFnm, abcSave, elmed, scoreFnm, timerId = -1;
    var ybplayer, yubchk = 0, pbrates = [], noprogress = 0, onYouTubeAPIContinue, opt_url = {}, sok = null, gFac;
    var dummyPlayer = new DummyPlayer(), TOFF = 0.01, beginVol = 0.7;
    var dottedHeight = 30, topSpace = 10, deNot, hasSmooth = 0;
    var hOff = 10;  // x positions are from box outlines, add hOff to appox. real position
    opt = {};       // global options
    var optdef = {  // default values
        'jump': 0, 'no_menu': 1, 'repufld': 0, 'noplyr': 1, 'nocsr': 0, 'media_height': '', 'btns': 1, 'ipadr': '',
        'mstr': 0, 'autscl': 1, 'ctrmed': 0, 'ctrnot': 1, 'lncsr': 0, 'opacity': 0.2, 'synbox': 0, 'speed': 1.0,
        'top_margin': 0, 'yubvid': '', 'nomed': 0, 'delay': 0, 'repskip': 0, 'spdctl': 0, 'lopctl': 0, 'metro': 0,
        'btime': -1, 'etime': 0, offrol: 0, dotted: 0
    }
    onYouTubeIframeAPIReady = yubApiReady;
    var rMarks = [];    // a marker for each voice
    var isvgPrev = [];  // svg index of each marker
    var gCurMask = 0;   // cursor mask (0-255)
    var playLstIx = 0;  // play list index

    function initPreload() {
        opt = Object.assign(opt, optdef);  // copy the default values
        $('#yubuse').prop('checked', false);
        $('#yvdlbl, #vidyub').css('display', 'none');
        msc_credits = undefined;
        $('#credits').html('');
        media_height = undefined;           // backwards compatibility (reset from previous preload)
        $('#buttons').css('height', '');    // remove explicit height from previous preload
        mediaFnm = '';
        yubchk = 0;
        elmed = null;
    }

    function initGlobals() {
        abcSave = '';       // abc code:: [string]
        muziek = '';        // svg code generated by abc2svg
        curmtr = [0, 0, 0];   // current metre
        curtmp = [0, 0, 0];   // current tempo
        msc_svgs = []       // svg elements, one per line of music
        msc_gs = []         // top graphics elements <g></g> of all music lines
        msc_wz = null;      // gobal cursor object
        offset = 0.0;       // offset: time in media file where music starts
        gFac = 0.1;         // absolute change for offset or duration
        noprogress = 0;     // stop cursor until offset synced
    }

    function Wijzer(xss, ymins, ymaxs, times, tixlb, lbtix, tixbts) {  // create the music cursor
        this.xs = xss;          // [[x coor of barlines] for each line]
        this.ymin = ymins;      // [y coor of top staff for each line]
        this.ymax = ymaxs;      // idem bottom staff
        this.times = times;     // [[music time at barlines] for each line]
        this.times.unshift(0); // add a starting time of zero
        this.tixbts = tixbts;   // remember beats per played measure
        this.line = 0;          // current music line index
        this.msre = 1;          // current measure index
        this.width = 0;         // width of svg music line
        this.wijzer = $(document.createElementNS('http://www.w3.org/2000/svg', 'svg'));
        this.wijzer.attr('id', 'wijzer');
        this.wijzer.css('overflow', 'visible');
        this.shade = $(document.createElementNS('http://www.w3.org/2000/svg', 'rect'));
        this.shade.attr({width: '100%', height: '100%'});
        this.shade.attr('id', 'shade');
        this.wijzer.append(this.shade);
        this.tiktak = $(document.createElementNS('http://www.w3.org/2000/svg', 'text'));
        this.tiktak.attr('y', 5);
        this.tiktak.css({fill: 'green', stroke: 'green', 'text-anchor': 'end', 'font-size': 'xx-large'});
        this.wijzer.append(this.tiktak);
        this.atag = $(document.createElementNS('http://www.w3.org/2000/svg', 'text'));
        this.atag.attr('id', 'atag');
        this.atag.text('<');
        this.atag.css({fill: 'red', stroke: 'red', 'text-anchor': 'middle'});
        this.btag = $(document.createElementNS('http://www.w3.org/2000/svg', 'text'));
        this.btag.attr('id', 'btag');
        this.btag.text('>');
        this.btag.css({fill: 'red', stroke: 'red', 'text-anchor': 'middle'});
        if (typeof (lpRec) == 'undefined') {    // skip if defined in preload
            lpRec = {loopBtn: 1, loopStart: 0, loopEnd: times [times.length - 1]} // end of music
            $('#lopctl').prop('checked', false);  // clear option and menu item
            opt.lopctl = 0;
        }
        this.hmargin = 100;     // horizontal scroll margin
        this.vmargin = 50;      // vertical scroll margin
        this.tmargin = opt.top_margin >= 0 ? opt.top_margin : this.vmargin; // margin bewteen media and score
        this.lastSync = 0;      // last measure that was synced.
        this.setScale();       // offsets of music lines (w.r.t. top notation area) and scale factor (g-coors -> pixels)
        this.cursorTime = 0;    // time position of cursor for redraw when no media loaded
        this.time_ix = 1;       // points to end time of first measure in this.times
        this.tixlb = tixlb;     // time index -> [line_num, bar_index, repcnt]
        this.lbtix = lbtix;     // line_num, bar_index, repcnt -> time index
        this.repcnt = 1;        // count of total number of repeats (-> unique passage index)
        this.noCursor = 0;      // hide cursor
        this.nseqCur = 0;       // current position in this.ntsSeq
        this.tAbcLast = 0;      // last ABC time of the cursor
    }

    Wijzer.prototype.setline = function (line) {
        $('#wijzer').remove(); // take away cursor from where it was
        this.sety(this.ymin [line], this.ymax [line]);
        this.line = line;
        this.wijzer.prependTo(msc_gs [line]);  // insert cursor in target music line
        this.width = msc_svgs [line].width.baseVal.value;
        var ntop = deNot.scrollTop;
        if (opt.dotted) {
            var ybalk = $('#shade')[0].getBoundingClientRect().top;
            var yrol = $('#rollijn')[0].getBoundingClientRect().top + dottedHeight;
            ntop = ntop + ybalk - yrol;
        } else {
            var ymx = ntop + deNot.clientHeight - this.vmargin;    // bottom of notation area
            if (this.line_offsets [line + 1] > ymx || this.line_offsets [line] < ntop + this.vmargin) {
                ntop = this.line_offsets [line] - this.tmargin;
            }
        }
        return ntop;
    }
    Wijzer.prototype.sety = function (ymin, ymax) { // set height, width and top y-coor of music cursor
        this.wijzer.attr('y', ymin.toFixed(2));   // top of first staff
        this.wijzer.attr('width', '2');
        this.wijzer.attr('height', (ymax - ymin).toFixed(2));
        this.shade.attr('fill', 'blue');
    }
    Wijzer.prototype.setx = function (x, xleft, xright) { // horizontal position in music line
        var nleft = deNot.scrollLeft;
        var xmx = nleft + deNot.clientWidth - this.hmargin; // right most side of notation area
        if (opt.lncsr) {
            //~ this.wijzer.attr ('x', x.toFixed (2));
            //~ this.wijzer.attr ('width', '2');
            //~ this.shade.attr ('fill-opacity', this.noCursor ? '0.0' : '0.5');
            this.wijzer.attr('width', '0');
            x = x / this.scale;                     // g-coors -> pixels for scroll test
            if (x > xmx || x < nleft + this.hmargin) {
                nleft = x > this.hmargin ? x - this.hmargin : 0;
            }
        } else {
            this.wijzer.attr('x', xleft.toFixed(2));
            this.wijzer.attr('width', (xright - xleft).toFixed(2));
            this.shade.attr('fill-opacity', this.noCursor ? '0.0' : '' + opt.opacity);
            xleft = xleft / this.scale;             // g-coors -> pixels for scroll test
            xright = xright / this.scale;
            if (xright > xmx || xleft < nleft + this.hmargin) {
                nleft = xleft > this.hmargin ? xleft - this.hmargin : 0;
            }
        }
        return deNot.scrollWidth > deNot.clientWidth ? nleft : 0;   // when scrolling is possible
    }
    Wijzer.prototype.time2x = function (t, rondaf, noAnim) {
        if (noprogress) return; // stop cursor until offset synced
        this.cursorTime = t;
        var times, line, ts, xs, msre, tix, deTop;
        times = this.times;
        tix = this.time_ix;
        while (tix < times.length && t > times [tix]) tix += 1;
        if (opt.etime && t + offset > opt.etime || !opt.etime && tix == times.length) {
            if (yubchk) {
                if (elmed.getPlayerState() == 1) elmed.pauseVideo();
            } else {
                if (!elmed.paused) elmed.pause();
            }
            $('body').trigger('play_end')
            return;     // music duration exceeds score duration or given end time -> pause
        }
        while (tix > 0 && t < times [tix - 1]) tix -= 1;
        if (rondaf && times [tix] - t < 0.3) {      // and times [tix] - t >= 0, by while loop above
            times [tix] = t - TOFF;                 // correct timing !!
            //console.log ('tijdcor: ' + (t - TOFF) + ', maat: ' + tix);
            if (tix < times.length - 1) tix += 1;   // t now in the next measure
        }
        if (opt.metro && tix != this.time_ix) metronome(tix, t);
        this.time_ix = tix;
        this.repcnt = this.tixlb [tix][2];
        msre = this.tixlb [tix][1];
        this.msre = msre;
        line = this.tixlb [tix][0];
        deTop = this.line != line ? this.setline(line) : deNot.scrollTop;

        var tleft, tright, xleft, xright, x, lastTime, i, j, n, nleft;
        xs = this.xs [line];
        tleft = times [tix - 1];
        tright = times [tix];
        xleft = xs [msre - 1] + hOff;
        xright = xs [msre] + hOff;  // x positions are from box outlines, add hOff to appox. real position
        var f = (t - tleft) / (tright - tleft);    // time progress in measure [tix]
        x = xleft + (xright - xleft) * f;
        lastTime = this.times [this.times.length - 1];
        if (t <= 0 || t > lastTime) nleft = this.setx(0, 0, 0);   // hide cursor if t not within score
        else nleft = this.setx(x, xleft, xright);
        doeRol(nleft, deTop, noAnim);
        if (opt.synbox) {
            this.showSyncInfo();
        }

        if (opt.lncsr) {
            var mix = this.tix2mix [tix - 1];
            var tabcLeft = this.barTimes [mix];
            var tABC = tabcLeft + (this.barTimes [mix + 1] - tabcLeft) * f;
            if (tABC < this.tAbcLast) this.nseqCur = 0;  // rewound: search from begin
            for (i = this.nseqCur; i < this.ntsSeq.length; ++i) {   // find tABC in ABC note sequence
                n = this.ntsSeq [i];
                if (n.t < tABC && i < this.ntsSeq.length - 1) continue
                for (j = this.nseqCur; j < i; ++j)  // mark all notes upto current one
                    putMarkLoc(this.ntsSeq [j]);
                this.nseqCur = i;                   // remember current index for next tick
                break;
            }
            this.tAbcLast = tABC;
        } else {
            rMarks.forEach(function (mark) {   // verwijder oude markeringen
                var pn = mark.parentNode;
                if (pn) pn.removeChild(mark);
            });
        }
    }
    Wijzer.prototype.drawTags = function () {
        for (var k in {atag: 1, btag: 1}) {
            if (!(k in lpRec)) continue;
            var a = lpRec [k];
            this [k].prependTo(msc_gs [a.line]);
            this [k].attr('x', a.x);
            this [k].attr('y', this.ymin [a.line]);
        }
    }
    Wijzer.prototype.doLoopTag = function (x, line, t, ix, tix) {
        function putTag(tag, x, line, next, mark, t, ix, tix) {
            if (!opt.lncsr) {   // round click position to start or end of measure
                var xs = that.xs [line], ts = that.times;
                var xleft = xs [ix - 1], xright = xs [ix];
                var tleft = ts [tix - 1], tright = ts [tix];
                if (lpRec.loopStart == tleft + TOFF) {
                    tag = 'btag';
                    mark = 'loopEnd';
                } // when mark already there put the other one
                if (lpRec.loopEnd == tright - TOFF) {
                    tag = 'atag';
                    mark = 'loopStart';
                }
                if (mark == 'loopStart') {
                    x = xleft;
                    t = tleft + TOFF;
                } else {
                    x = xright;
                    t = tright - TOFF;
                }
            }
            lpRec [tag] = {x: x.toFixed(2), line: line};
            lpRec.loopBtn = next;
            lpRec [mark] = t;
            that.drawTags();
        }

        var d1, d2, that = this;
        switch (lpRec.loopBtn) {
            case 1:
                putTag('atag', x, line, 2, 'loopStart', t, ix, tix);
                break;
            case 2:
                if (t > lpRec.loopStart) putTag('btag', x, line, 3, 'loopEnd', t, ix, tix);
                break;
            case 3:                         // reposition mark closest to time of click location
                d1 = Math.abs(lpRec.loopStart - t);
                d2 = Math.abs(lpRec.loopEnd - t);
                if (d1 < d2) putTag('atag', x, line, 3, 'loopStart', t, ix, tix);
                else putTag('btag', x, line, 3, 'loopEnd', t, ix, tix);
        }
    }
    Wijzer.prototype.x2time = function (x, line) {
        var xs, ts, ix, xleft, xright, tleft, tright, t, tix;
        x = x * this.scale;
        xs = this.xs [line];
        ix = 1;
        if (x < xs [0]) {
            keyDown({key: ' '});
            return;
        }        // position before first bar line
        while (ix < xs.length && xs [ix] < x) ix += 1;
        if (ix == xs.length) {
            keyDown({key: ' '});
            return;
        }   // position beyond last bar line
        var msretix = this.lbtix [line][ix];
        if (!msretix [this.repcnt]) {                           // clicked outside repeat:
            var pnums = Object.keys(msretix);                  // passage numbers at this measure
            if (this.repcnt < pnums [0]) {                      // right of repeat section
                this.repcnt = parseInt(pnums [0]);             // take lowest passage number
            } else {                                            // left of repeat section
                this.repcnt = parseInt(pnums [pnums.length - 1]);  // take highest passage number
            }
        }
        tix = msretix [this.repcnt];                            // ix == bar number == 1..
        ts = this.times;
        xleft = xs [ix - 1];
        xright = xs [ix];
        tleft = ts [tix - 1];
        tright = ts [tix];
        t = tleft + (tright - tleft) * (x - xleft) / (xright - xleft);
        if (opt.lopctl) {
            this.doLoopTag(x, line, t, ix, tix);
        } else {
            if (opt.synbox && (yubchk ? elmed.getPlayerState() == YT.PlayerState.PLAYING : !elmed.paused)) {
                this.syncTimes(x, ix, line, tix);
            } else playPause2(false, (opt.lncsr ? t : tleft + TOFF) + offset);  // do not toggle player state
        }
    }
    Wijzer.prototype.goMsre = function (b) {    // one measure forwards (1) or backwards (0)
        var t, tix = this.time_ix;
        if (!elmed) return;
        if (b) t = this.times [tix] + TOFF;     // go just beyond end of current measure
        else {
            if (tix <= 2) t = TOFF;
            else t = this.times [tix - 2] + TOFF;    // tix - 2 >= 1
        }
        playPause2(false, t + offset);         // do not toggle player state
    }
    Wijzer.prototype.showSyncInfo = function () {
        var tix = this.time_ix;
        var msre_dur = this.times [tix] - this.times [tix - 1];
        $('#sync_info').html('duration&nbsp;measure:<br>' + msre_dur.toFixed(3) + ' sec.<br>');
        $('#sync_info').append('media&nbsp;offset:<br>' + offset.toFixed(3) + ' sec.');
    }
    Wijzer.prototype.changeTimesKeyb = function (gfac) {
        var mnum = this.lbtix [this.line] [this.msre][this.repcnt] - 1; // the cursor is in measure mnum = tix - 1 = 0..
        this.changeTimes(mnum, gfac, 0);
    }
    Wijzer.prototype.changeTimes = function (mnum, dt, dur) {   // endtime += dt or endtime = begintime + dur
        var tix, tendnew, ts = this.times;
        for (tix = mnum + 1; tix < ts.length; ++tix) {
            tendnew = dur ? ts [tix - 1] + dur : ts [tix] + dt; // ts [tix-1] == begintime
            ts [tix] = tendnew;
        }
    }
    Wijzer.prototype.syncTimes = function (x, ix, line, tix) {  // click location, measure index, line number, time index
        var mnum, tbeg, tend, t, tleft;
        mnum = this.lbtix [line][ix][this.repcnt] - 1;          // mnum = 0..
        t = (yubchk ? elmed.getCurrentTime() : elmed.currentTime) - offset - 0.2;   // subtract a constant delay to compensate processing time
        tleft = t;
        if (mnum == 0) {            // click in first measure sets the offset
            offset += tleft;        // approximated new begin time -> offset
            yubchk ? elmed.seekTo(offset + TOFF, true) : elmed.currentTime = offset + TOFF;  // jump to start
            if (noprogress) $('#woff').click();    // cursor starts moving again
            return;
        }   // mnum >= 1, tix >= 2
        mnum -= 1;                  // change end time or duration of previous measure
        tbeg = tix == 2 ? 0 : this.times [tix - 2];
        tend = this.times [tix - 1];
        if (tleft < tbeg + 0.5) {   // 0.5 sec, beats per measure > 2 -> tempo > 240
            alert('tempo faster than 240 bpm: first sync previous measures');
            return
        }
        if (this.lastSync > mnum) { // only change duration of mnum and keep the rest unchanged
            this.changeTimes(mnum, tleft - tend, 0);
        } else {                    // change duration of all measures >= mnum (extrapolate current tempo)
            this.changeTimes(mnum, 0, tleft - tbeg);
            this.lastSync = mnum;
        }
        if (opt.jump) {
            yubchk ? elmed.seekTo(tbeg + offset + TOFF, true) : elmed.currentTime = tbeg + offset + TOFF;  // jump to start of previous measure
        }
    }
    Wijzer.prototype.setSize = function () {
        var i, svg, w_svg, h_svg, new_w;
        for (i = 0; i < msc_svgs.length; ++i) {
            svg = msc_svgs[i]
            w_svg = svg.width.baseVal.value;
            h_svg = svg.height.baseVal.value;
            new_w = $('#notation').prop('clientWidth');
            svg.width.baseVal.value = new_w;
            svg.height.baseVal.value = new_w * h_svg / w_svg;
        }
    }
    Wijzer.prototype.setScale = function () {
        var i, x, w_svg, w_vbx, scale, divoff, divscroll, m, svg = msc_svgs[0], topg;
        w_svg = svg.getBoundingClientRect().width;     // width svg element in pixels
        w_vbx = svg.viewBox.baseVal.width;              // width svg element (vbx coors)
        m = msc_gs[0].get(0).transform.baseVal;        // scale factor top g-grafic
        scale = m.numberOfItems ? m.getItem(0).matrix.a : 1;   // scale: svg-coors -> vbx-coors
        this.scale = ((w_vbx / scale) / w_svg);         // pixels -> svg-coors
        divoff = $('#notation').position();        // music area relative to offset parent
        divscroll = $('#notation').scrollTop();
        this.line_offsets = [];                     // [(top music line - top music area) for each line]
        for (i = 0; i < msc_svgs.length; ++i) {
            x = $(msc_svgs [i]).position();
            this.line_offsets [i] = divscroll + x.top - divoff.top;
        }
        this.line_offsets [i] = $('#notation')[0].scrollHeight - topSpace;  // next line would start here
    }

    Wijzer.prototype.compCountIn = function () {
        var count_in = {time: 0.25, num: 4};             // default for piece with one measure
        var b = this.time_ix > 1 ? this.time_ix - 1 : this.time_ix; // start at current measure but skip the very first
        var e = Math.min(this.times.length - 1, b + 3); // tempo is avarage of next three measures
        if (e > b) {
            var totbeats = this.tixbts.slice(b, e).reduce(function (x, y) {
                return x + y;
            }, 0)
            count_in.time = (this.times [e] - this.times [b]) / totbeats / opt.speed;
            count_in.num = this.tixbts [b];
        }
        return count_in;
    }

    function DummyPlayer() {
        this.paused = true;
        this.currentTime = 0;
        this.klok = -1;
        this.step = 200;
        this.playing = 0;
        initPbRates(0.1, 4, 0.05);
    }

    DummyPlayer.prototype.pause = function () {
        this.clearKlok();
        tick();
    }
    DummyPlayer.prototype.play = function () {
        return false;
        this.paused = false;
        if (this.klok != -1) return;  // play after play (when changing currentTime)
        var o = this;
        this.setKlok(function () {
            o.currentTime += (o.step / 1000) * opt.speed;
            tick();
        }, this.step);
    }
    DummyPlayer.prototype.setKlok = function (f, dt) {
        if (this.klok != -1) clearInterval(this.klok);
        this.klok = f ? setInterval(f, dt) : -1;   // setInterval () > 0
        this.paused = false;
        if (msc_wz && opt.nocsr) msc_wz.noCursor = 1;
    }
    DummyPlayer.prototype.clearKlok = function () {
        if (this.klok != -1) clearInterval(this.klok);
        this.klok = -1;
        this.paused = true;
        if (msc_wz) msc_wz.noCursor = 0;
    }

    function doeRol(nleft, deTop, noAnim) {
        var fLeft = nleft != deNot.scrollLeft;
        var fTop = deTop != deNot.scrollTop;
        if (!fLeft && !fTop) return;
        if (hasSmooth) {
            deNot.style ['scroll-behavior'] = noAnim || fLeft ? 'auto' : 'smooth';
            deNot.scroll(nleft, deTop);
        } else {
            if (fLeft) deNot.scrollLeft = nleft;
            if (fTop) $(deNot).animate({scrollTop: deTop});
        }
    }

    function toggleScoreBtn() {
        var b = $('#abclbl'), h = b.html(), c = $('#impbox').prop('checked');
        b.toggleClass('abcimp', c);
        b.html(c ? h.replace('score file', '<b>import</b>') : h.replace('<b>import</b>', 'score file'));
        if (c && !opt.btns) $('#btns').click();    // show file buttons (-> checkMenu -> btnChk etc.)
    }

    function copyTiming(xs, abctxt) {
        if (xs.indexOf('//# This page') < 0) {
            alert('not a preload file');
            return
        }
        xs = abctxt.replace(/\n/g, '');
        var r = xs.match(/offset_js = (.*);/);
        if (r.length > 1) offset = offset_js = parseFloat(r[1]);
        r = xs.match(/times_arr = (.*);abc_arr/);
        var times;
        if (r.length > 1) times = flattenTimes(JSON.parse(r[1]));
        if (msc_wz) {
            msc_wz.times = times;
            msc_wz.times.unshift(0);
        }
        $('#impbox').prop('checked', false);
        toggleScoreBtn();
    }

    function readAbcOrXML(abctxt) {
        var xs = abctxt.slice(0, 4000);    // only look at the beginning of the file
        if ($('#impbox').prop('checked')) {
            copyTiming(xs, abctxt);
            return;
        }
        if (xs.indexOf('//# This page') >= 0 || xs.indexOf('play_list') >= 0) {
            initPreload();                 // should run before preload file is executed
            eval(abctxt);                  // preload from source file button executes here
            for (var k in optdef) {         // backwards compatibility for old preload files
                if (!(k in opt)) opt [k] = optdef [k];  // new options not present in old preload
            }
            msc_check_preload();
            return;
        }
        if (xs.indexOf('X:') >= 0) {
            dolayout(abctxt);
            return
        }
        if (xs.indexOf('<?xml ') == -1) {
            alert('not an xml file nor an abc file');
            return
        }
        var xmldata = $.parseXML(abctxt);
        var options = {p: 'f', t: 1, u: 1 ? opt.repufld : 0, v: 3, mnum: 0}; // t==1 -> clef determines step value on staff
        var res = vertaal(xmldata, options);
        if (res[1]) $('#err').append(res[1] + '\n');
        dolayout(res[0]);
    }

    function readDbxFile(files) {
        $('#err').text('');    // clear error output area
        times_arr = undefined;  // clear possible preload data
        offset_js = undefined;
        lpRec = undefined;
        var url = files[0].link;
        scoreFnm = files[0].name.split('.')[0];
        $('#wait').toggle(true);
        $('#err').text('link: ' + url + '\n');
        $.get(url, '', null, 'text').done(function (data, status) {
            $('#err').append('preload: ' + status + '\n');
            abc_arr = data.split('\n');
            msc_check_preload();
        }).fail(function (jqxhr, settings, exception) {    // same origin policy
            $('#wait').append('\npreload failed: ' + settings);
        });
    }

    function readLocalFile(type, files) {
        $('#err').text('');    // clear error output area
        times_arr = undefined;  // clear possible preload data
        offset_js = undefined;
        if (!$('#impbox').prop('checked')) lpRec = undefined;  // not when we only read the timing data !!
        var freader = new FileReader();
        freader.onload = function (e) {
            readAbcOrXML(freader.result);
        }
        var f = type == 'dd' ? files [0] : $('#fknp').prop('files')[0];
        if (f) {
            scoreFnm = f.name.split('.')[0];
            freader.readAsText(f);
        }
    }

    function doDrop(e) {
        e.stopPropagation();
        e.preventDefault();
        $('body').toggleClass('indrag', false);
        var files = e.dataTransfer.files;
        if (/video|audio/.test(files [0].type))    // xml => 'text/xml', abc => ''
            readMedia('dd', files)
        else readLocalFile('dd', files);
    }

    function readMedia(type, files) {
        var f, url;
        if (type == 'dbx') {    // dropbox
            f = files[0];
            url = f.link;
        } else {                // type 'btn' or 'dd'
            f = type == 'dd' ? files [0] : $('#mknp').prop('files')[0];
            url = window.URL.createObjectURL(f);
        }
        setPlayer(f.name, url);
    }

    function readMediaYub() {
        if (!$('#yubid')[0].checkValidity()) {
            alert("The youtube video id should be 11 characters long,\neach from 'A' to 'Z', 'a' to 'z', '0' to '9', '-' or '_'");
            return;
        }
        opt.yubvid = $('#yubid').val();
        setPlayer('', '');
    }

    function initPbRates(min, max, inc) {
        pbrates = [];
        for (var x = min; x <= max + 0.001; x += inc) { // 0.001 because error in javascript integers !!
            x = Math.round(x * 100) / 100;
            pbrates.push(x);
        }
    }

    function yubApiReady() {
        function klaar() {
            $('#yubuse').prop('checked', true);
            medbtnSwitch();
            yubload();
        }

        function toestand(evt) {
            if (evt.data == YT.PlayerState.PLAYING) {
                dummyPlayer.setKlok(tick, 100);
                setSpeed(0);   // only now the player honours the speed setting
            } else dummyPlayer.pause();
            if (evt.data == YT.PlayerState.CUED) {
                setNotationHeight();
            }
        }

        ybplayer = new YT.Player('vidyub', {events: {'onReady': klaar, 'onStateChange': toestand}});
    }

    function yubload(f) {
        if (f) onYouTubeAPIContinue = f;

        function grey(b) {
            $('#yubuse').attr('disabled', b);
            $('#yublbl').css('color', b ? '#aaa' : '#000');
            $('#yubload').toggle(b);
        }

        if (typeof (YT) == 'undefined') {
            grey(true);
            $('#yubuse').prop('checked', false);
            $.getScript("https://www.youtube.com/iframe_api");
        } else {
            grey(false);
            onYouTubeAPIContinue();
        }
    }

    function setPlayer(fnm, mediaSrc) {
        mediaSrc = mediaSrc.replace('www.dropbox', 'dl.dropboxusercontent').split('?')[0];    // make direct link
        mediaFnm = mediaSrc.indexOf('http') == 0 ? mediaSrc : fnm; // URL to the dropbox file | local file name
        var $elmed;
        fnm = fnm.split('?')[0];           // strip url parameter when using dropbox (?dl=1)
        $('#vid, #aud').attr('src', '');    // stop current media
        if (ybplayer) ybplayer.stopVideo();
        dummyPlayer.clearKlok();           // stop running Dummy player -> clearInterval
        var play_start = opt.btime >= 0 ? opt.btime : offset
        if (!fnm) {                         // youtube
            yubchk = 1;
            $('#vid, #aud').css('display', 'none');
            $('#vidyub').css('display', 'inline-block');
            $('#buttons').toggleClass('video', true);
            yubload(function () {
                elmed = ybplayer;
                pbrates = elmed.getAvailablePlaybackRates();
                setSpeed(0);
                setNotationHeight();   // redundant, but with iOS no cued event (after url-preload)
                elmed.cueVideoById({videoId: opt.yubvid, startSeconds: play_start});
                elmed.setVolume(beginVol * 100);
            });
        } else {
            yubchk = 0;
            if (/\.webm$|\.mp4$/i.test(fnm)) {
                $elmed = $('#vid');
                if ($elmed.length == 0) return; // video not supported
                $('#vidyub, #aud').css('display', 'none');
                $elmed.css('display', 'inline-block');
                $('#buttons').toggleClass('video', true);
            } else {
                $elmed = $('#aud');
                if ($elmed.length == 0) return; // audio not supported
                $('#vidyub, #vid').css('display', 'none');
                $elmed.css('display', 'inline-block');
                $('#buttons').toggleClass('video', false);
            }
            elmed = $elmed.get(0);
            if (/\.ogg$/i.test(mediaSrc)) {    // may be also mp3 file present
                if (!elmed.canPlayType('audio/ogg')) mediaSrc = mediaSrc.replace(/\.ogg$/i, '.mp3');
            }
            if (/\.webm$/i.test(mediaSrc)) {
                if (!elmed.canPlayType('video/webm')) mediaSrc = mediaSrc.replace(/\.webm$/i, '.mp4');
            }
            $elmed.attr('src', mediaSrc);
            $elmed.on('playing', function () {
                dummyPlayer.setKlok(tick, 100);    // timeupdate event is too slow, use our own timer
                elmed.playbackRate = opt.speed;
            });
            $elmed.on('pause', function () {
                dummyPlayer.pause()
            });
            $elmed.on('loadedmetadata', function () {
                setNotationHeight();
                elmed.currentTime = play_start;
            });
            elmed.volume = beginVol;
            initPbRates(0.5, 2, 0.05);
            setSpeed(0);
            setNotationHeight();   // redundant, but with iOS no cued event (after url-preload)
        }
    }

    function medbtnSwitch() {
        var yb = $('#yubuse').prop('checked');
        $('#medlbl').css('display', yb ? 'none' : 'block');
        $('#yvdlbl').css('display', yb ? 'block' : 'none');
    }

    function centerPlayer() {
        var ww = parseInt($('body').css('width')) / 2;
        var wv = parseInt($('#vid').css('width')) / 2;
        var wy = parseInt($('#vidyub').css('width')) / 2;
        var wa = parseInt($('#aud').css('width')) / 2;
        $('#vid').css('margin-left', opt.ctrmed ? (ww - wv).toFixed() + 'px' : '0px');
        $('#vidyub').css('margin-left', opt.ctrmed ? (ww - wy).toFixed() + 'px' : '0px');
        $('#aud').css('margin-left', opt.ctrmed ? (ww - wa).toFixed() + 'px' : '0px');
        $('#meddiv').css('text-align', opt.ctrmed ? 'left' : 'center');
    }

    function setNotationHeight() {
        if (elmed) $(elmed).toggle(!opt.noplyr);
        $('#buttons').toggleClass('noheight', !!opt.noplyr);    // !! -> jquery wants real boolean
        var chk = $('#btns').prop('checked');
        var h = parseFloat($('#buttons').css('height'));
        var b = parseFloat($('body').css('height'));
        var e = chk ? parseFloat($('#err').css('height')) : 0;
        var x = 100 - (100 * (h + e) / b).toFixed();
        $('#notation').css('height', x + '%');   // fill all space below media
        $('#vidyub').css('width', (h * 0.95 * 1.6).toFixed());   // youtube player needs width, aspect 1.6
        centerPlayer();
        alignCursor();
    }

    function dolayout(abctxt) {
        var muziek = '', errtxt = '', abc2svg, bxs = {}, bys = {}, bars = [], xleft, times = [], nxs = [], mtxts = [];
        var BAR = 0, METER = 6, NOTE = 8, REST = 10, TEMPO = 14, BASE_LEN = 1536, tixbts = [], mbeats = [], mreps = [],
            mdurs = [];
        var tixlb = [[0, 0, 1]];          // time index -> [line_num, bar_num, repcnt], line_num == 0.., bar_num == 1.., repcnt = 1..
        var lbtix = [];                 // line_num, bar_num, repcnt -> time index
        var nSvg = 0;
        var barTimes = [0], noteTimes = [], ntsPos = {}, tix2mix = [], ntsSeq, lastNote;

        function errmsg(txt, line, col) {
            errtxt += txt + '\n';
        }

        function keySort(d) {
            var keys = Object.keys(d).map(function f(x) {
                return parseFloat(x);
            });
            keys.sort(function f(a, b) {
                return a - b;
            });    // numerical sort
            return keys;
        }

        function img_out(str) {
            if (str.indexOf('<svg') != -1) {
                str = str.replace(/width="(\d*)px"\s*height="(\d*)px"/, 'width="$1px" height="$2px" viewbox="0 0 $1 $2"');
                bxs = keySort(bxs);
                bys = keySort(bys);
                if (bxs.length > 1 &&   // the first barline is at bxs[1] because bxs[0] == left side staff
                    bxs[1] < Math.min.apply(null, nxs)) {  // first barline < min x-coor of all notes in this line
                    bxs.splice(0, 1);  // remove left side staff because there already is a left barline
                }
                bxs = bxs.filter(function (x, i, xs) { // filter values that are too close to make a real measure
                    return i == 0 || x - xs [i - 1] > 15;
                });
                bars.push({'xs': bxs, 'ys': bys});
                bxs = {};
                bys = {};
                nxs = [];
                nSvg += 1;
            }
            muziek += str;
        }

        function svgInfo(type, s1, s2, x, y, w, h) {
            if (type == 'note' || type == 'rest') {
                nxs.push(abc2svg.ax(x));  // x-coor of notes/rests for left barline check
                x = Math.round(abc2svg.ax(x) * 100) / 100;
                y = Math.round(abc2svg.ay(y) * 100) / 100;
                h = abc2svg.ah(h);
                ntsPos [s1] = [nSvg, x, y, w, h];
            }
            if (type == 'bar') {
                x = abc2svg.ax(x);
                y = abc2svg.ay(y);
                h = abc2svg.ah(h);
                bxs [x] = 1;
                bys [y] = 1;        // top of staff
                bys [y + h] = 1;    // bottom of staff
                xleft = abc2svg.ax(0);
                bxs [xleft] = 1;
            }
        }

        function getTune(abctxt) {
            var ts, t, abc_lines, i, ro;
            abctxt = abctxt.replace(/\r\n/g, '\n'); // \r\n matches /^$/ ==> each line would get an extra empty line!!!
            ts = abctxt.split(/^\s*X:/m);  // split on X:, multi line search
            if (ts.length == 1) return [];  // no X:
            t = ts[1].split(/^\s*$/m);     // split on empty lines
            t = ts[0] + 'X:' + t[0];        // header + first tune
            abc_lines = t.split(/\r\n|[\n\r\u0085\u2028\u2029]/);  // whoppa
            for (i = 0; i < Math.min(100, abc_lines.length); ++i) {
                ro = abc_lines [i].match(/%%scale\s*([\d.]+)/);    // avoid %%scale 1.0, because different svg hierarchy
                if (ro && ro[1] == 1.0) abc_lines [i] = '%%scale 0.99';
            }
            return abc_lines;
        }

        function timeLine(ts_p, voice_tb, music_types) {
            var ts, g, ftempo = 384 * 120 / 60, dtmp, mdur = 0, mt = 0, nbeat, lbtm = 0;    // quarter duration 384, tempo 120
            try {
                nbeat = voice_tb [0].meter.a_meter [0].top;
            } // first voice, first meter: {top: x, bot: y}
            catch (e) {
                nbeat = '4';
            }      // no meter defined in abc
            for (ts = ts_p; ts; ts = ts.ts_next) {
                switch (ts.type) {
                    case NOTE:
                    case REST:
                        noteTimes.push({time: ts.time, iabc: ts.istart, vce: ts.v, inv: ts.invis});
                }
                if (ts.v != 0) continue;    // skip voices > 0
                switch (ts.type) {
                    case TEMPO:
                        dtmp = ts.tempo_notes.reduce(function (sum, x) {
                            return sum + x;
                        });
                        ftempo = dtmp * ts.tempo / 60;
                        break;
                    case NOTE:
                    case REST:
                        mdur += ts.dur / ftempo;
                        break;
                    case BAR:
                        //~ console.log ('bar_type: ' + ts.bar_type + ' text: ' + ts.text);
                        if ('soln' in ts) lbtm = ts.time;   // to detect left bar at start of line
                        if (ts.time == lbtm) {
                            mreps [mreps.length - 1] += ts.bar_type;
                            break;
                        }    // concatenate left bar with previous
                        mdurs.push(mdur);
                        mdur = 0;
                        nbeat = nbeat.replace('C|', '2').replace('C', '4');
                        mbeats.push(parseInt(nbeat));    // array of beats per measure
                        mreps.push(ts.bar_type);
                        mtxts.push(ts.text);
                        barTimes.push(ts.time);
                        break;
                    case METER: // assume 4/4 when no meter is given
                        nbeat = ts.a_meter.length ? ts.a_meter [0].top : '4'
                        break;
                }
            }
            rMarks.forEach(function (mark) {   // verwijder oude markeringen
                var pn = mark.parentNode;
                if (pn) pn.removeChild(mark);
            });
            isvgPrev = [];                      // clear svg indexes
            var kleur = ['#f9f', '#3cf', '#c99', '#f66', '#fc0', '#cc0', '#ccc'];
            var nVoices = voice_tb.length;
            for (var i = 0; i < nVoices; ++i) { // a marker for each voice
                var alpha = 1 << i & gCurMask ? '0' : ''
                var rMark = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
                rMark.setAttribute('fill', kleur [i % kleur.length] + alpha);
                rMark.setAttribute('fill-opacity', '0.5');
                rMark.setAttribute('width', '0');  // omdat <rect> geen standaard HTML element is werkt rMark.width = 0 niet.
                rMarks.push(rMark);
                isvgPrev.push(-1);
            }
        }

        var percSvg = ['%%beginsvg\n<defs>',
            '<text id="x" x="-3" y="0">&#xe263;</text>',
            '<text id="x-" x="-3" y="0">&#xe263;</text>',
            '<text id="x+" x="-3" y="0">&#xe263;</text>',
            '<text id="normal" x="-3.7" y="0">&#xe0a3;</text>',
            '<text id="normal-" x="-3.7" y="0">&#xe0a3;</text>',
            '<text id="normal+" x="-3.7" y="0">&#xe0a4;</text>',
            '<g id="circle-x"><text x="-3" y="0">&#xe263;</text><circle r="4" class="stroke"></circle></g>',
            '<g id="circle-x-"><text x="-3" y="0">&#xe263;</text><circle r="4" class="stroke"></circle></g>',
            '<path id="triangle" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="stroke-width:1.4"></path>',
            '<path id="triangle-" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="stroke-width:1.4"></path>',
            '<path id="triangle+" d="m-4 -3.2l4 6.4 4 -6.4z" class="stroke" style="fill:#000"></path>',
            '<path id="square" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="stroke-width:1.4"></path>',
            '<path id="square-" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="stroke-width:1.4"></path>',
            '<path id="square+" d="m-3.5 3l0 -6.2 7.2 0 0 6.2z" class="stroke" style="fill:#000"></path>',
            '<path id="diamond" d="m0 -3l4.2 3.2 -4.2 3.2 -4.2 -3.2z" class="stroke" style="stroke-width:1.4"></path>',
            '<path id="diamond-" d="m0 -3l4.2 3.2 -4.2 3.2 -4.2 -3.2z" class="stroke" style="stroke-width:1.4"></path>',
            '<path id="diamond+" d="m0 -3l4.2 3.2 -4.2 3.2 -4.2 -3.2z" class="stroke" style="fill:#000"></path>',
            '</defs>\n%%endsvg'];

        function perc2map(abcIn) {
            var fillmap = {'diamond': 1, 'triangle': 1, 'square': 1, 'normal': 1};
            var abc = percSvg, ls, i, x, r, id = 'default', maps = {'default': []}, dmaps = {'default': []};
            ls = abcIn.split('\n');
            for (i = 0; i < ls.length; ++i) {
                x = ls [i];
                if (x.indexOf('I:percmap') >= 0) {
                    x = x.split(' ').map(function (x) {
                        return x.trim();
                    });
                    var kop = x[4];
                    if (kop in fillmap) kop = kop + '+' + ',' + kop;
                    x = '%%map perc' + id + ' ' + x[1] + ' print=' + x[2] + ' midi=' + x[3] + ' heads=' + kop;
                    maps [id].push(x);
                }
                if (x.indexOf('%%MIDI') >= 0) dmaps [id].push(x);
                if (x.indexOf('V:') >= 0) {
                    r = x.match(/V:\s*(\S+)/);
                    if (r) {
                        id = r[1];
                        if (!(id in maps)) {
                            maps [id] = [];
                            dmaps [id] = [];
                        }
                    }
                }
            }
            var ids = Object.keys(maps).sort();
            for (i = 0; i < ids.length; ++i) abc = abc.concat(maps [ids [i]]);
            id = 'default';
            for (i = 0; i < ls.length; ++i) {
                x = ls [i];
                if (x.indexOf('I:percmap') >= 0) continue;
                if (x.indexOf('%%MIDI') >= 0) continue;
                if (x.indexOf('V:') >= 0 || x.indexOf('K:') >= 0) {
                    r = x.match(/V:\s*(\S+)/);
                    if (r) id = r[1];
                    abc.push(x);
                    if (id in dmaps && dmaps [id].length) {
                        abc = abc.concat(dmaps [id]);
                        delete dmaps [id];
                    }
                    if (x.indexOf('perc') >= 0 && x.indexOf('map=') == -1) x += ' map=perc';
                    if (x.indexOf('map=perc') >= 0 && maps [id].length > 0) abc.push('%%voicemap perc' + id);
                    if (x.indexOf('map=off') >= 0) abc.push('%%voicemap');
                } else abc.push(x);
            }
            return abc.join('\n');
        }

        function compPlayMap() {
            var line = 0, // (system) line index: 0..,
                ibar = 1,   // measure index on line: 1..
                nbars = bars [line].xs.length,  // number of measures on this line
                mix = 0,    // total measure index: mix = 0.., total time index: tix = 1..
                pbtime = 0, // play back time
                reptix = 1, // total time index of start of repeat    (count includes repeats)
                repmix = 0, // total measure index of start of repeat (count excludes repeats)
                repnum = 1, // 2 in second traversal
                repcnt = 1, // total number of repeats, unique passage index
                volta = 0,  // 1..
                repdone = {};   // mark executed repeats (for nested repeats)
            while (1) {
                var v = mtxts [mix - 1];              // volta is on the previous measure
                var r = v ? v.match(/[,\d]*(\d)/) : null;  // last int is highest volta num
                if (r) {
                    v = parseInt(r[1]);
                    if (v != volta) volta = v;      // volta lasts until next volta
                }
                if (!volta || volta >= repnum)      // skip when repnum > volta num
                {
                    tix2mix.push(mix);
                    pbtime += mdurs [mix];
                    times.push(pbtime);
                    tixbts.push(mbeats [mix]);     // also unfold beats for metronome and count-in
                    if (!lbtix [line]) lbtix [line] = [];
                    if (!lbtix [line][ibar]) lbtix [line][ibar] = [];
                    lbtix [line][ibar][repcnt] = tixlb.length;
                    tixlb.push([line, ibar, repcnt]);
                }
                if (mreps [mix] != '|') volta = 0;  // reset on any special bar line
                r = /^:/.test(mreps [mix]);
                if (r && repnum == 1 && !repdone [mix] && !opt.repskip) { // jump to start of repeat
                    repnum = 2;                     // now second play
                    repcnt += 1;                    // unique repeat number for lbtix and tixlb
                    repdone [mix] = 1               // only execute repeats once (when nested)
                    mix = repmix;
                    ibar = tixlb [reptix][1];       // bar index on this line
                    line = tixlb [reptix][0];       // line index
                    nbars = bars [line].xs.length;  // number of measures on this line
                } else {
                    if (r) repnum = 1;              // reset repcount
                    if (/:$/.test(mreps [mix])) {  // define start of repeat
                        reptix = tixlb.length;
                        repmix = mix + 1;
                        repnum = 1;                 // first play
                    }
                    mix += 1;                       // go to next measure
                    ibar += 1;
                    if (ibar >= nbars) {            // measure is on next line
                        ibar = 1;                   // first bar index on this line
                        line += 1;                  // next line
                        if (line >= bars.length) break; // end of part
                        nbars = bars [line].xs.length;  // number of measures on this line
                    }
                }
            }
        }

        initGlobals();
        var score = $('#notation');
        $('body').attr('title', '') // clear drag/drop help message
        score.empty();
        score.append('<div style="height:' + topSpace + 'px">&nbsp;</div>');
        var abc_lines = getTune(abctxt);
        abctxt = abc_lines.join('\n');
        if (abctxt.indexOf('percmap') >= 0) abctxt = perc2map(abctxt);
        var user = {
            'img_out': img_out,
            'errmsg': errmsg,
            'read_file': function (x) {
                return '';
            },   // %%abc-include, unused
            'anno_start': svgInfo,
            'get_abcmodel': timeLine
        }
        abc2svg = new Abc(user);
        abc2svg.tosvg('abc2svg', abctxt);
        if (errtxt != '') $('#err').append(errtxt);
        score.append(muziek);
        score.append('<div style="height:' + topSpace + 'px">&nbsp;</div>');
        msc_svgs = score.find('svg');  // all music lines
        msc_svgs.each(function () {        // take the <svg> element for drawing, but
            var e = $(this).find('.g');    // if a <g class="g"> is present, take that for drawing
            msc_gs.push(e.length ? e : $(this));
        });
        var wz_xs = [], wz_ymin = [], wz_ymax = [];
        for (var i = 0; i < bars.length; ++i) { // i = line number
            var bs = bars [i];          // bars of line i
            wz_xs [i] = bs.xs;          // x coors of bars
            wz_ymin [i] = bs.ys [0];    // min, max y coor of bar
            wz_ymax [i] = bs.ys [bs.ys.length - 1];
        }
        compPlayMap();
        //~ console.log ('times: ' + times);
        //~ console.log ('tixbts: ' + tixbts);
        //~ console.log ('mreps: ' + mreps);
        //~ console.log ('mtxts: ' + mtxts);
        //~ console.log ('tixlb: ' + JSON.stringify (tixlb));
        //~ console.log ('lbtix: ' + JSON.stringify (lbtix));
        //~ console.log ('len times: ' + times.length + ' len tixlb: ' + tixlb.length)
        if (typeof (times_arr) != 'undefined') {    // external timings take precedence.
            times = flattenTimes(times_arr);       // make 1d array of times
        }
        if (typeof (offset_js) != 'undefined') {
            offset = offset_js;     // external offset
        }
        if (opt.offset) offset = opt.offset;    // (URL) parameter takes precedence
        abcSave = abc_lines;   // keep in global for saving
        msc_wz = new Wijzer(wz_xs, wz_ymin, wz_ymax, times, tixlb, lbtix, tixbts);
        msc_svgs.each(function () {
            $(this).mousedown(klik);
        });  // each music line gets the click handler
        if (!elmed) elmed = dummyPlayer;
        setTimeout(function () {   // wait on DOM rendering ready
            setLoop();             // set loop markers
            alignCursor();         // scroll to first measure
        }, 0);
        ntsSeq = noteTimes.map(function (n) {
            return {t: n.time, vce: n.vce, xy: ntsPos [n.iabc]}
        });
        lastNote = ntsSeq [ntsSeq.length - 1];
        lastNote.t += 1;        // increase time just beyond last note
        ntsSeq.push(lastNote); // dummy extra note for searching in time2x
        msc_wz.ntsSeq = ntsSeq;
        msc_wz.barTimes = barTimes;
        msc_wz.tix2mix = tix2mix;
    }

    function putMarkLoc(n) {
        var p, isvg, x, y, w, h, mark, pn;
        mark = rMarks [n.vce];
        p = n.xy;
        if (!p) {   // n.xy == undefined
            mark.setAttribute('width', 0);
            mark.setAttribute('height', 0);
            return;
        }
        isvg = p[0];
        x = p[1];
        y = p[2];
        w = p[3];
        h = p[4];
        if (n.inv) {
            w = 0;
            h = 0;
        }    // markeer geen onzichtbare rusten/noten
        if (isvg != isvgPrev [n.vce]) {
            pn = mark.parentNode;
            if (pn) pn.removeChild(mark);
            pn = msc_gs [isvg][0];
            pn.insertBefore(mark, pn.firstChild);
            isvgPrev [n.vce] = isvg;
        }
        mark.setAttribute('x', x);
        mark.setAttribute('y', y);
        mark.setAttribute('width', w);
        mark.setAttribute('height', h);
    }

    function flattenTimes(times) {  // the number of measure per line may have changed since synchronization.
        var ts = times.map(function (x) {
            return x.slice(1);
        });      // cut begin times (doubles endtime prev line)
        ts = ts.reduce(function (acc, x) {
            return acc.concat(x);
        });  // glue all lines together -> [time_value]
        return ts;
    }

    function tick() {
        if (!elmed) return;
        var t = (yubchk ? elmed.getCurrentTime() : elmed.currentTime) - offset, tcur = t;
        if (t < 0) t = 0;
        if (opt.lopctl) {
            if (t > lpRec.loopEnd) t = lpRec.loopStart;
            if (t < lpRec.loopStart) t = lpRec.loopStart + TOFF;
            if (t != tcur) yubchk ? elmed.seekTo(t + offset, true) : elmed.currentTime = t + offset;
        }
        if (msc_wz) msc_wz.time2x(t, 0, 0);
    }

    function klik(evt) {       // mousedown on svg
        evt.preventDefault();
        evt.stopPropagation();
        var line = msc_svgs.get().indexOf(this);   // index of the clicked svg
        var x = evt.clientX;    // position click relative to page
        var r = this.getBoundingClientRect()
        x -= r.left + hOff;
        msc_wz.x2time(x, line);
    }

    function syncChk() {
        $('#sync_out').css('display', opt.synbox ? 'block' : 'none');
        if (msc_wz && opt.synbox) msc_wz.showSyncInfo();
    }

    function btnChk() {
        toggleBtns();  // -> setNotationHeight ()
        if (opt.btns && typeof (FileReader) == 'undefined') {
            $('#notation').prepend('<h3>Your browser does not support reading of local files ...</h3>but you can use the preload feature.');
        }
    }

    function lineChk() {
        if (msc_wz) {
            msc_wz.nseqCur = 0; // reset line cursor search
            alignCursor();     // redraw cursor
        }
        $('#notation svg').css('margin-left', opt.ctrnot ? 'auto' : '0px');
        $('#notation svg').css('margin-right', opt.ctrnot ? 'auto' : '0px');
    }

    function hideSpeedChk() {
        var spd = $('#spdctl').prop('checked');    // show/hide speed
        $('#spdlbl').css('display', spd ? 'block' : 'none');
    }

    function dropuse() {
        function grey(b) {
            $('#drpuse').prop('checked', !b);
            $('#drpuse').attr('disabled', b);
            $('#drplbl').css('color', b ? '#aaa' : '#000');
        }

        if (typeof (Dropbox) == 'undefined') {
            grey(true);
            var u = 'https://www.dropbox.com/static/api/2/dropins.js';
            $.ajax({url: u, dataType: 'script', cache: true}).done(function () {
                grey(false);
                Dropbox.init({appKey: 'ckknarypgq10318'});
                loaddrop();
                dropuse();
            });
        } else {
            var du = $('#drpuse').prop('checked');
            $('.dropbox-dropin-btn').css('display', du ? 'inline-block' : 'none');
            $('#fknp, #mknp').css('display', du ? 'none' : 'inline-block');
        }
    }

    function toggleBtns() {
        $('#medbts').css('display', opt.btns ? 'inline' : 'none');
        $('#err').css('display', opt.btns ? 'block' : 'none');
        setNotationHeight();
    }

    function metronome(tix, t) {
        var num, tik, dt, tb;
        clearInterval(in_count_in);
        in_count_in = 0;

        function telaf() {
            if (tik <= num) {
                in_count_in = setTimeout(telaf, dt);
                msc_wz.tiktak.text(tik);
                tik += 1;
            }
        }

        num = msc_wz.tixbts [tix - 1];
        tik = 1;
        dt = (msc_wz.times [tix] - t) / num / opt.speed * 1000;
        in_count_in = setTimeout(telaf, 0);
    }

    function clear_metronome() {
        if (!msc_wz) return;
        setTimeout(function () {   // use the event queue to avoid race condition with new telaf cycle
            clearInterval(in_count_in);
            in_count_in = 0;
            msc_wz.tiktak.text('');
        }, 0);
    }

    var in_count_in = 0;    // timer id as semaphoor
    function do_count_in(cmd, delay) {
        function reset() {
            $('#countin').toggle(false);
            clearInterval(in_count_in);
            in_count_in = 0;
        }

        function telaf() {
            $('#countin').html('<b>' + ci.num + '</b>').toggle(true);
            if (ci.num-- == 0) {
                reset();
                playPause(cmd, delay);
            }
        }

        if (in_count_in) {
            reset();
            return;
        }
        cmd = cmd.replace(':true', ':false');  // reset count_in request
        var ci = msc_wz.compCountIn();         // -> ci.time, ci.num
        telaf();
        in_count_in = setInterval(telaf, ci.time * 1000);
    }

    function playPause(cmd, delay) {  // cmd = do_pause : at_time
        if (!elmed) return;
        var x = cmd.split(':'), toggle = x [0] == 'true', time = parseFloat(x [1]), fcount_in = x [2] == 'true';
        var yubstate = yubchk ? elmed.getPlayerState() : 0;
        var paused = yubchk ? yubstate != 1 : elmed.paused;
        yubchk ? yubstate != 5 && elmed.seekTo(time, true) : elmed.currentTime = time;  // always position (except when cued)
        if (paused && toggle || !paused && !toggle) {  // play
            if (fcount_in) {   // play after count in
                do_count_in(cmd, delay);
                return;
            }
            if (delay) {        // play after delay
                setTimeout(function () {
                    playPause(cmd, 0);
                }, delay);
                return;
            }
            yubchk ? elmed.playVideo() : elmed.play();
            if (opt.metro && msc_wz) msc_wz.time_ix = 0; // to get the metronome running
        } else {    // pause
            yubchk ? yubstate != 5 && elmed.pauseVideo() : elmed.pause(); // do not pause when cued
            if (opt.metro) clear_metronome(); // clear metronome when paused
        }
        var rondaf = !opt.lncsr && !toggle;    // shading & only positioning & close to end bar -> correct bar time
        if (msc_wz) msc_wz.time2x(time - offset, rondaf, 0);
    }

    function playPause2(toggle, t) {   // toggle player state : go to t : with count in, only user actions come here
        var cmd = toggle + ':' + t.toFixed(2) + ':' + (toggle && $('#cntin').prop('checked'));
        if (sok) sok.send(cmd); else playPause(cmd, 0);   // synchronize when socket connection
    }

    function keyDown(e) {
        function toggleSpeedLoop() {
            if (opt.spdctl && !opt.lopctl) $('#lopctl').click();
            else if (opt.lopctl && !opt.spdctl) $('#spdctl').click();
            else $('#spdctl, #lopctl').click();
        }

        var key = e.key;
        var done = 1;
        switch (key) {
            case 'ArrowLeft':
            case 'Left':
                msc_wz.goMsre(0);
                break;
            case 'ArrowRight':
            case 'Right':
                msc_wz.goMsre(1);
                break;
            case 'Spacebar':
            case ' ':
                if (e.preventDefault) e.preventDefault();
                if (!elmed) break;
                playPause2(true, yubchk ? elmed.getCurrentTime() : elmed.currentTime) // toggle player state
                break;
            case 'a':
                $('#autscl').click();
                break;
            case 'f':
                $('#btns').click();
                break;
            case 'h':
                $('#help').toggleClass('showhlp');
                break;
            case 'l':
                $('#lncsr').click();
                break;
            case 'm':
                $('#menu label').toggle();
                break;
            case 's':
                toggleSpeedLoop();
                break;
            case '+':
                setSpeed(1);
                break;
            case '-':
                setSpeed(-1);
                break;
            default:
                done = 0;
                break;
        }
        if (!opt.synbox || !msc_wz || done) return;
        switch (key) {
            case ',':
                if (e.ctrlKey) {
                    offset += gFac;
                } else msc_wz.changeTimesKeyb(gFac);
                break;  // ,
            case '.':
                if (e.ctrlKey) {
                    offset -= gFac;
                } else msc_wz.changeTimesKeyb(-gFac);
                break; // .
            case 'w':
                saveTiming();
                break;                  // w
        }
        e.preventDefault();    // our keys no default action
        msc_wz.showSyncInfo();
    }

    function msc_resize() {
        centerPlayer();
        if (!msc_wz) return;
        var w_svg = msc_svgs[0].width.baseVal.value;    // width svg element on screen (px)
        var force = w_svg > screen.width;               // only force with small devices
        if (!force && !opt.autscl) {
            alignCursor(true);
            return;
        }
        msc_wz.setSize.call(msc_wz);   // resize svg width to notation area width. Use msc_wz as "this".
        msc_wz.setScale.call(msc_wz);
        alignCursor(true);
    }

    function encMap(abc) {
        var xs = abc.map(function (x) {
            return window.btoa(unescape(encodeURIComponent(x)));
        }).join('++');
        var ys = [], n = 0;
        while (n <= xs.length) {
            ys.push(xs.substr(n, 150));
            n += 150;
        }
        return ys
    }

    function decMap(encabc) {
        function dec(sx) {
            return encabc.join('').split(sx).map(function (x) {
                return decodeURIComponent(escape(window.atob(x)));
            });
        }

        var s;
        try {
            s = dec('++');
        } catch (e) {
            s = dec('+');
        }    // backwards compatibility
        return s.join('\n');
    }

    function saveTiming() {
        var a, ts, tss = [], fnm, of, res, tprev = '[', tlast, cdt, abcpln, abcenc, os, lpm;
        fnm = 'media_file = "' + (yubchk ? '' : mediaFnm) + '";\n'; // only when h5player active
        cdt = typeof (msc_credits) != 'undefined' ? 'msc_credits = ' + JSON.stringify(msc_credits) + ';\n' : '';
        of = 'offset_js = ' + offset.toFixed(2) + ';\n';
        opt.synbox = 0; // do not save enable sync
        os = 'opt = ' + JSON.stringify(opt) + ';\n';
        opt.synbox = 1; // because we are still in sync mode
        lpm = lpRec.loopBtn ? 'lpRec = ' + JSON.stringify(lpRec) + ';\n' : '';
        ts = msc_wz.times.map(function (x) {
            return x.toFixed(2);
        })
        while (ts.length) { // make two dim array for backwards compatibility
            tlast = ts [9]; // duplicate the last element as the first of the next row
            tss.push(tprev + ts.splice(0, 10).join(',') + ']');
            tprev = '[' + tlast + ',';
        }
        ts = 'times_arr = [' + tss.join(',\n') + '];\n';
        if ($('#encr').prop('checked')) {
            abcenc = encMap(abcSave).map(function f(x) {
                return JSON.stringify(x);
            });
            abcpln = ['"X:1"'];
        } else {
            abcenc = [''];
            abcpln = abcSave.map(function f(x) {
                return JSON.stringify(x);
            });
        }
        abcenc = 'abc_enc = [' + abcenc.join(',\n') + '];\n';
        abcpln = 'abc_arr = [' + abcpln.join(',\n') + '];\n';

        res = '//########################################\n'
        res += '//# This page contains score data, timing data and the media file path. Save it as a text file in\n'
        res += '//# the same folder as abcweb.html. Abcweb preloads score and media when it is opened with the\n'
        res += '//# file name as parameter in the url, for example: http://your.domain.org/abcweb.html?file_name\n'
        res += '//# Also works locally with file:///path/to/abcweb.html?file_name\n'
        res += '//# **** You have to correct the path to the media file below! (media_file="...";) ****\n'
        res += '//########################################\n//#\n'
        res += fnm + cdt + of + os + lpm + ts + abcpln + abcenc;

        var dataUrl = 'data:text/plain;charset=utf-8;base64,' + btoa(unescape(encodeURIComponent(res)));
        if ($('#drpuse').prop('checked')) {
            var options = {
                success: function () {
                    $('#err').text('"' + scoreFnm + '.js" saved to your Dropbox.\n');
                },
                progress: function (progress) {
                },
                cancel: function () {
                },
                error: function (errorMessage) {
                    $('#err').text("Error: " + errorMessage + '\n');
                    $('#err').append('fnm: ' + scoreFnm + ', len: ' + dataUrl.length + '\n');
                }
            };
            $('#err').text('');
            Dropbox.save(dataUrl, scoreFnm + '.js', options);
        } else {
            try {
                a = document.createElement('a');   // don't bother to use jquery
                a.href = dataUrl;
                a.download = scoreFnm + '.js';
                a.text = "Save synchronization data"
                $('#saveDiv').append(a); // append to a dummy invisible div
                a.click(); // only seems to work if a is appended somewhere in the body
            } catch (err) {
                if (!confirm('Do you want to save your synchronization data?')) return;
                document.open("text/html");    // clears the whole document and opens a new one
                document.write('<pre>' + res + '</pre>');
                document.close();
            }
        }
    }

    function msc_preload(preparms) {
        initPreload();     // should run before preload
        $('#err').text('');
        var parstr, xmlfnm = '', preload = '', elm, r, p, ps, i, m = '', host;
        parstr = window.location.href.replace('?dl=0', '').split('?'); // look for parameters in the url;
        if (preparms) parstr = ['', preparms];
        if (r = parstr [0].match(/:\/\/([^/:]+)/)) host = r [1];
        if (parstr.length > 1) {    // preload media and score
            ps = parstr [1].split('&');
            for (i = 0; i < ps.length; i++) {
                p = ps [i].replace(/d:(\w{15}\/[^.]+\.)/, 'https://dl.dropboxusercontent.com/s/$1');
                if (r = p.match(/xml=(.*)/)) xmlfnm = decodeURIComponent(r [1]).replace('www.dropbox', 'dl.dropboxusercontent');
                else if (r = p.match(/med=(.*)/)) m = r [1];
                else if (r = p.match(/tmr=(\d*)/)) opt_url.top_margin = parseInt(r [1]);
                else if (r = p.match(/mht=(\d*)/)) opt_url.media_height = parseInt(r [1]);
                else if (r = p.match(/tb=([\d.]*)/)) opt_url.btime = parseFloat(r [1]);
                else if (r = p.match(/te=([\d.]*)/)) opt_url.etime = parseFloat(r [1]);
                else if (r = p.match(/off=([+-]?[\d.]+)/)) opt_url.offset = parseFloat(r [1]);
                else if (r = p.match(/ip=(\d+.\d+.\d+.\d+)/)) opt_url.ipadr = r [1];
                else if (r = p.match(/^d([\d.]+)$/)) opt_url.delay = parseFloat(r[1]);
                else if (p.match(/ip=host/) && host) opt_url.ipadr = host;
                else if (p == 'mstr') opt_url.mstr = 1;
                else if (p == 'jmp') opt_url.jump = 1;
                else if (p == 'syn') opt_url.synbox = 1;
                else if (p == 'nb') opt_url.no_menu = 1;
                else if (p == 'sp') opt_url.spdctl = opt_url.lopctl = 1;
                else if (p == 'ur') opt_url.repufld = 1;
                else if (p == 'npl') opt_url.noplyr = 1;
                else if (p == 'ncr') opt_url.nocsr = 1;
                else if (p == 'asc') opt_url.autscl = 1;
                else if (p == 'cm') opt_url.ctrmed = 1;
                else if (p == 'cs') opt_url.ctrnot = 1;
                else if (p == 'nomed') {
                    opt_url.nomed = 1;
                    opt_url.noplyr = 1
                } else preload = p;
                if (/(\.xml$)|(\.abc$)/.test(preload)) {
                    xmlfnm = preload;
                    preload = '';
                }
                if (/(\.mp3$)|(\.mp4$)|(\.ogg$)|(\.webm$)/.test(preload)) {
                    m = preload;
                    preload = '';
                }
            }
            if (m) {
                if (m.length == 11 && m.indexOf('.') == -1) opt.yubvid = m;
                else media_file = decodeURIComponent(m).replace('www.dropbox', 'dl.dropboxusercontent');
            }
            if (preload || xmlfnm) $('#wait').toggle(true);
            if (xmlfnm) {   // force loading xml as plain text
                $.get(xmlfnm, '', null, 'text').done(function (data, status) {
                    $('#err').append('preload: ' + status + '\n');
                    abc_arr = data.split('\n');
                    msc_check_preload();
                }).fail(function (jqxhr, settings, exception) {    // same origin policy
                    $('#wait').append('\npreload failed: ' + settings);
                });
            } else if (preload) {   // get the javasript preload file
                if (preload.indexOf('dropbox.com') >= 0) preload += '?dl=1';
                $.getScript(preload).done(function (data, status) {
                    $('#err').append('preload: ' + status + '\n');
                    msc_check_preload();
                }).fail(function (jqxhr, settings, exception) {    // same origin policy, but ...
                    $('#wait,#err').append('preload failed: ' + exception + ', trying script tag ...\n');
                    elm = document.createElement('script');
                    elm.src = preload;
                    elm.onload = function () {
                        msc_check_preload();
                    };
                    elm.onerror = function () {
                        $('#wait').append('\npreload failed');
                    };
                    document.head.appendChild(elm);    // execute cross domain javascript; security? what security?
                    document.head.removeChild(elm);
                });

            }
        }
        return preload || xmlfnm;
    }

    function msc_check_preload() {
        if (playLstIx == 0 && play_list) {  // new playlist read
            $('body').trigger('play_end'); // play first file
            return;
        }
        if (typeof (abc_arr) != 'undefined') {
            var abc_string = abc_arr.join('\n')
            if (typeof (abc_enc) != 'undefined' && abc_enc.length) {
                abc_string = decMap(abc_enc);
                opt.no_menu = 1;
            }
            readAbcOrXML(abc_string);  // always defined in preload -> starts another msc_check_preload after "eval ()" which redefines abc_arr;
        }
        for (var id in opt_url) opt [id] = opt_url [id];            // options in URL take precedence
        if ('nospd' in opt) {       // translate for backwards compatibility
            opt.spdctl = !opt.nospd;
            opt.nospd = undefined;  // avoid saving the old option
        }
        if (typeof (media_file) != 'undefined' && media_file && !opt.nomed) {
            setPlayer(media_file, media_file);
            opt.btns = 0;
        }
        if (opt.yubvid && !opt.nomed) {
            setPlayer('', '');
            opt.btns = 0;
        }
        if (typeof (msc_credits) != 'undefined') {
            var xs = msc_credits.reduce(function (acc, x) {
                return acc + x;
            });
            $('#credits').html(xs);
        }
        if (typeof (media_height) != 'undefined') opt.media_height = media_height;  // backwards compatibility (reset in initPreload)
        if (opt.no_menu) {
            $('#sync').css('display', 'none'); // hide all buttons
            opt.btns = 0;   // when nb used as url parameter
            $('body').off('dragenter dragleave drop dragover');
            $('body').on('contextmenu', function (e) {
                e.preventDefault();
            });
            if (elmed.controlsList) elmed.controlsList.add('nodownload');
        }
        $('#wait').toggle(false);
        resetIntf(false);
    }

    function resetIntf(copy_options) {
        var id;
        if (copy_options) for (id in opt_url) opt [id] = opt_url [id];  // for url with only parameters
        if (opt.ipadr) webSokOpen(opt.ipadr);
        if (opt.media_height) $('#buttons').css('height', opt.media_height);
        if (opt.offrol) $('#rollijn').css('top', opt.offrol);
        if (opt.offset) offset = opt.offset;
        for (id in opt) $('#' + id).prop('checked', opt [id]); // set all checkboxes
        btnChk();      // -> toggleBtns () -> setNotationHeight ()
        lineChk();     // set lineCursor
        hideSpeedChk();
        syncChk();     // show sync_box+info when sync enabled
        msc_resize();  // resize score when opt.autscl == true
        $('#medbts, #meddiv').css('visibility', 'visible');
        $('#rollijn').css('display', opt.dotted ? 'block' : 'none');
        if (playLstIx) keyDown({key: ' '});
    }

    function logerr(s) {
        $('#err').append(s + '\n');
    }

    function webSokOpen(ip) {
        if (sok) {
            logerr('websocket already open');
            return;
        }
        var url = 'ws://' + ip + ':' + 8091 + '/';
        sok = new WebSocket(url);
        sok.onmessage = function (event) {
            //~ logerr (event.data);
            if (event.data == 'master') $('#mbar').css('background', 'rgba(255,0,0,0.2)');
            else playPause(event.data, opt.delay * 100);   // initial latency in 0.1 second units
        }
        sok.onerror = function (event) {
            logerr('socket error (server inaccessible?)');
            sok = null;
        }
        sok.onopen = function (event) {
            $('#mbar').css('background', 'rgba(0,255,0,0.2)');
            if (opt.mstr) sok.send('master');
            logerr('connection opened');
        }
        sok.onclose = function (event) {
            $('#mbar').css('background', '');
            logerr('connection closed: ' + event.code);
            sok = null;
        }
    }

    function msc_shift(evt) {
        if (hideMenuHelp()) return;
        if (evt.target.id != 'meddiv' && evt.target.id != 'credits') return;
        evt.preventDefault();
        evt.stopPropagation();
        $('#buttons').css('opacity', '0.5');
        $('#streep').css('opacity', '1.0');
        var touchDev = evt.type == 'touchstart';
        var doel = $('#meddiv');
        var y1 = touchDev ? evt.originalEvent.touches[0].clientY : evt.pageY;
        var bh = $('#buttons').height();
        doel.css('cursor', 'row-resize')
        doel.on('mousemove touchmove', function (evt) {
            var h = $('body').height();
            var y2 = touchDev ? evt.originalEvent.touches[0].clientY : evt.pageY;
            var y = bh + y2 - y1;
            var p = 100 * y / h;    // height percentage
            opt.media_height = p.toFixed() + '%'
            $('#buttons').css('height', opt.media_height);
        });
        doel.on('mouseup touchend', function (evt) {
            $('#buttons').css('opacity', '1.0');
            $('#streep').css('opacity', '0.0');
            doel.off('mousemove touchmove mouseup touchend');
            doel.css('cursor', 'initial')
            setNotationHeight();
        });
    }

    function lijn_shift(evt) {
        evt.preventDefault();
        var touchDev = evt.type == 'touchstart';
        $('#rollijn').toggleClass('rolgroen')
        var doel = touchDev ? $('#rollijn') : $('body');
        doel.on(touchDev ? 'touchmove' : 'mousemove', function (evt) {
            var h = $('#notation').offset().top;
            var y = touchDev ? evt.originalEvent.touches[0].clientY : evt.clientY;
            var yp = 100 * (y - dottedHeight / 2) / document.body.clientHeight;
            opt.offrol = yp.toFixed(2) + '%';  // keep cursor in the middle of rollijn
            $('#rollijn').css('top', opt.offrol);
            alignCursor(true); // no animation
        });
        doel.on(touchDev ? 'touchend' : 'mouseup', function (evt) {
            doel.off('mousemove touchmove mouseup touchend');
            $('#rollijn').toggleClass('rolgroen')
        });
    }

    function setSpeed(inc) {
        if (inc == 2) {     // response to change event
            var newspeed = $('#speed').val();
            var dif = newspeed - opt.speed;
            if (Math.abs(dif) <= 0.06) inc = dif > 0 ? 1 : -1; // probably step-up/down
            else {
                opt.speed = newspeed;
                inc = 0;
            }             // probably manual setting
        }
        var ix = pbrates    // get the index of the playback rate closest to opt.speed
            .map(function (x, i) {
                return {x: Math.abs(x - opt.speed), i: i};
            })
            .sort(function (a, b) {
                return a.x - b.x;
            }) [0].i;
        if (inc == -1 && ix > 0) opt.speed = pbrates [ix + inc];
        if (inc == 1 && ix < pbrates.length - 1) opt.speed = pbrates [ix + inc];
        if (inc == 0) opt.speed = pbrates [ix];
        $('#speed').val(opt.speed.toFixed(2));
        if (elmed && !yubchk) elmed.playbackRate = opt.speed;
        if (elmed && yubchk) elmed.setPlaybackRate(opt.speed);
    }

    function setLoop() {
        if (msc_wz) msc_wz.drawTags();
        opt.lopctl = $('#lopctl').prop('checked');
        $('#atag').css('display', opt.lopctl ? 'block' : 'none');
        $('#btag').css('display', opt.lopctl ? 'block' : 'none');
    }

    function loaddrop() {
        var dknp1 = Dropbox.createChooseButton({
            success: readDbxFile,
            cancel: function () {
            }, linkType: "direct", multiselect: false,
            extensions: ['.xml', '.abc', '.txt', '.js']
        });
        var dknp2 = Dropbox.createChooseButton({
            success: function (farr) {
                readMedia('dbx', farr);
            },
            cancel: function () {
            }, linkType: "preview", multiselect: false,
            extensions: ['.ogg', '.mp3', '.webm', '.mp4']
        });
        $('#abcfile').append(dknp1);
        $('#mediafile').append(dknp2);
    }

    function checkMenu(evt) {
        var chk = $(this).prop('checked');
        var id = $(this).attr('id');
        opt [id] = chk;  // update the option object
        switch (id) {
            case 'ctrnot':
                lineChk();
                break;
            case 'ctrmed':
                centerPlayer();
                break;
            case 'spdctl':
                hideSpeedChk();
                break;
            case 'autscl':
                msc_resize();
                break;
            case 'lncsr':
                lineChk();
                break;
            case 'btns':
                btnChk();
                break
            case 'synbox':
                syncChk();
                break;
            case 'noplyr':
                setNotationHeight();
                break;
            case 'nocsr':
                if (msc_wz && !dummyPlayer.paused) msc_wz.noCursor = chk;
                break; // immediately hide/show while playing
            case 'metro':
                if (!chk) clear_metronome();
                break;
            case 'dotted':
                alignCursor();
                break;
        }
    }

    function hideMenu() {
        $('#menu label').css('display', 'none');
    }

    function hideMenuHelp() {          // called from click event dispatcher -> the change event could come later
        var b = $('#menu label').css('display') != 'none' || $('#help').hasClass('showhlp');
        if (b) {
            $('#help').toggleClass('showhlp', false);
            setTimeout(hideMenu, 0);   // wait until menu change event is dispatched -> doReadPdf = 1
        }
        return b
    }

    function setFullscreen() {
        var e = document.body;
        var fscrAan = e.requestFullscreen || e.mozRequestFullScreen || e.webkitRequestFullscreen;
        var fscrUit = document.exitFullscreen || document.mozCancelFullScreen || document.webkitExitFullscreen;
        if (!fscrAan || !fscrUit) return;
        if ($('#fscr').prop('checked')) fscrAan.call(e);
        else fscrUit.call(document);
    }

    function alignCursor(noAnim) {
        $('#rollijn').css('display', opt.dotted ? 'block' : 'none');
        if (!msc_wz) return;
        isvgPrev.forEach(function (_, i, xs) {
            xs [i] = -1;    // force insertion of marks in putMarkLoc
        });
        msc_wz.line = -1;   // force setline
        msc_wz.time2x(msc_wz.cursorTime, 0, noAnim);
    }

    window.loadSheet = function (filename) {
        deNot = document.getElementById('notation');
        hasSmooth = CSS.supports('scroll-behavior', 'smooth');
        $('#drpuse').prop('checked', false);
        if (!msc_preload()) {
            resetIntf(true);
        }
        $(window).resize(msc_resize);
        $('body').keydown(keyDown);
        $('#save').click(saveTiming);
        $('#speed').change(function () {
            setSpeed(2);
        });
        $('#lopctl').click(setLoop);
        var vtxt = '<a href="http://wim.vree.org/js/">abcweb</a> (version: ' + VERSION + ')</br>©Willem Vree'
        vtxt += '<br>using:<br><a href="http://moinejf.free.fr/js/">abc2svg</a>, ©Jef Moine'
        $('#help').prepend('<div style="position: absolute; right: 5px;">' + vtxt + '</div>');
        $('#helpm').click(function () {
            $('#help').toggleClass('showhlp');
        });
        $('#rollijn').on('mousedown touchstart', lijn_shift);

        $("#toggle_cursor").click(function () {
            $('#lncsr').click();
        });

        $("#notation").click(function () {
            console.log(elmed.currentTime);
        });

        window.sheet_offset = 0;

        window.go_to_time = function (time) {
            //console.log(elmed.currentTime);
            time = parseFloat(time) + parseFloat((window.sheet_offset /1000));
            playPause2(false, time);
        }

        function load_sheet_file(filename) {
            $.ajax({
                url: '/api/change_setting?setting_name=download_sheet_music&value=' + filename,
                success: function (data) {
                    readAbcOrXML(data);
                    lineChk();
                    msc_resize();
                },
                error: function () {
                    console.log("error");
                }
            });
        }

        load_sheet_file(filename);


        $('#fknp').change(function () {
            readLocalFile('btn', []);
        });
        $('#mknp').change(function () {
            readMedia('btn', []);
        });
        $('#yknp').click(readMediaYub);
        $('#yubid').keydown(function (e) {
            e.stopPropagation();
        });   // prevent bubble up to shortcut actions
        $('#yubuse').change(medbtnSwitch);
        $('#drpuse').click(dropuse);
        $('#notation').mousedown(function () {
            if (hideMenuHelp()) return;
            keyDown({key: ' '});
        });
        $('#jump').change(checkMenu);
        $('#impbox').change(toggleScoreBtn);
        $('#menu * input').change(checkMenu);      // for all menu checkboxes
        $('#menu label').toggle();                 // hide all menu items
        $('#mbar').click(function () {
            if ($('#menu label').css('display') == 'none') $('#menu label').toggle(true);
            else hideMenu();
        });
        $('#woff').change(function () {
            noprogress = $(this).prop('checked');
        });
        $.event.props.push("dataTransfer");          // make jQuery copy the dataTransfer attribute
        $('body').on('drop', doDrop);
        $('body').on('dragover', function (e) {   // this handler makes the element accept drops and generate drop-events
            e.stopPropagation();
            e.preventDefault();  // the preventDefault is obligatory for drag/drop!
            e.dataTransfer.dropEffect = 'copy';         // show a plus sign to indicate the file is copied
        });
        $('body').on('dragenter dragleave', function () {
            $(this).toggleClass('indrag');
        });
        $('#fscr').on('change', setFullscreen);
        $('body').on('fullscreenchange webkitfullscreenchange mozfullscreenchange', function () {
            var e = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement;
            $('#fscr').prop('checked', e != null);
        });
        $('body').on('play_end', function () {
            if (playLstIx >= play_list.length) return;   // no more files to play
            msc_preload(play_list [playLstIx]);
            playLstIx += 1
        });
    }
})();
